// eddata - education data analysis and visualization
edd.vars = [];  // data variables
    // e.g. edd.vars.var1 = {  // var1 is variable name
    // v: [v0, v1, v2, ... vn],
    // prefer: 'type',
    // label: 'label string',
    // etc.
    // };  // each one is an eddata type
edd.steps = [];
    // edd.step = [
    //   { // step 0
    //     include: [],
    //     show: {
    //       what: 'variables'
    //     }
    //   }, // end step 0
    //   { // step 1
    //     include: [
    //       edd.vars.mcas_subject_code: [10]
    //     ],
    //     show: {
    //       what: edd.vars.mcas_performance_levels,
    //       by: [edd.vars.mcas_year],
    //       how: {
    //         view: 'performance level history',
    //         normalize: false
    //       }
    //     }
    //   }
    // ];
edd.step = 0;
edd.include = [];  // row indices (for current step)
// Need to cache summaries?
// AYP targets, Massachusetts
edd.ma.aypStateTarget = {  // state AYP CPI targets for ELA and math
year: [2002, 2004, 2006, 2008, 2010, 2012, 2014],	
10: [70.7, 75.6, 80.5, 85.4, 90.2, 95.1, 100],	
20: [53, 60.8, 68.7, 76.5, 84.3, 92.2, 100]
};
//
// Education data types, with inheritance
//
function EdDatum(v) {
    // Generic education data type.
    // The object returns its primitive value in 'value' or 'string' contexts.
    // Each ed data type has a prefered type (defaulting to 'string') and label (defaulting to 'ed data').
    // Ed data types can set a regular expression in validExp and numeric range constraints in validRange.
    // Ed data types can also set the validNull property to false to invalidate missing values (with the default validate method).
    // The default validate method uses any validExp, validRange, and validNull settings.
    // Ed data types can override the fix method to provide typical data fixups (when fix() is explicitly invoked).
    // The default fix method converts the value to the preferred type.
    v = new Array(v)
    this.prefer = 'string';  // keep this compatible with Google DataTable column types
    this.label = 'ed data';
    this.valueOf = function() {return this.value};
    this.toString = function() {return String(this.value)};
    this.validExp = null;  // override with regular expression if appropriate
    this.validRange = {min: null, max: null, integer: null};  // set min and/or max and/or boolean 'integer' to enable checking
    this.validNull = true;  // set false to invalidate missing values
    this.validate = edDatumValidate;
    this.validateAll =
    this.fix = edDatumFix;
}

function edDatumValidate(i) {
    // Default validate method, to be run in context, via its call() method.
    //   E.g., edDatumValidate.call(this, v).
    // Validate regular expression, validNull, and any range parameters present.
    var first, last;
    var results = {};
    if (i !== undefined)
        first = last = i;
    else
        first = 0, last = this.v.length - 1;
    for (var iVal = first; iVal <= last; iVal++)

    var v = this.value;
    if (v == null) return this.validNull;
    var match = this.validExp == null || this.validExp.test(v);
    var inRange = this.validRange == null 
        ||     (this.validRange.min == null || v >= this.validRange.min) 
        && (this.validRange.max == null || v <= this.validRange.max)
        && (this.validRange.integer == null || (v - Math.floor(v) == 0) == this.validRange.integer);
    return match && inRange;
}

function edDatumFix() {
    // Default fix method, to be run in context.
    // Return null --> not implemented; 0 --> value okay; 1 --> fixed; -1 --> could not fix.
    var wasValid = this.validate();
    switch (this.prefer) {
        case 'string':
            this.value = String(this.value);
            break;
        case 'number':
            this.value = Number(this.value);
            break;
    }
    if (this.validate())
        return wasValid ? 0 : 1;
    return -1;
}

function MAOrgCode(code) {
    // MA organization ID, a string of eight digits, often with leading zeros.
    // Values are required, by default.
    // The fix method will add leading zeros to a shorter number string.
    EdDatum.call(this, code);
    this.value = String(code);
    this.label = 'MA organization code';
    this.validExp = /^\d{8}$/;  // eight digits
    this.validNull = false;
    this.fix =
        function() {
            var digitsExp = /^\d+$/;
            if (this.validate()) return 0;
            this.value = String(this.value);
            if (digitsExp.test(this.value) && this.value.length < 8) {
                // Prepend zeros.
                while (this.value.length < 8)
                    this.value = '0' + this.value;
                return 1;
            }
            return -1;
        };
}
MAOrgCode.prototype = new EdDatum;

function StudentID(id) {
    // Student ID, stored as a string.
    // Value required.
    EdDatum.call(this, id);
    this.value = String(id);
    this.label = 'student ID';
    this.validNull = false;
}
StudentID.prototype = new EdDatum;

function MASASID(sasid) {
    // MA SASID, state assigned student identifier, 10 digits.
    // Value required.
    StudentID.call(this, sasid);
    this.label = 'SASID';
    this.validExp = /^\d{10}$/;  // ten digits
}
MASASID.prototype = new StudentID;

function Name(value) {
    // A person's name.
    EdDatum.call(this, value);
    this.label = 'name';
}
Category.prototype = new EdDatum;

function Teacher(value) {
    Name.call(this, value);
    this.label = 'teacher';
}
Teacher.prototype = new Name;

function CalendarYear(year) {
    // As opposed to SchoolYear.
    // Valid as an integer between 1900 and 3000.
    // Values are required, by default.
    // The fix method will add 2000 to values less than 100.
    // The asDate method returns a Javascript date of January 1 in the given year.
    EdDatum.call(this, year);
    this.value = numberOrMissing(year);
    this.prefer = 'number';
    this.label = 'calendar year';
    this.validRange.min = 1900;
    this.validRange.max = 3000;
    this.validRange.integer = true;
    this.validNull = false;
    this.fix = 
        function() { 
            if (this.value < 100 && this.validate(this.value + 2000)) { 
                this.value += 2000;
                return 1;
            }
            return this.validate() ? 0 : -1;
        };
    this.asDate = 
        function(mo, da) {
            !mo && (mo = 1);
            !da && (da = 1);
            return new Date(this.value, mo, da);
        };
}
CalendarYear.prototype = new EdDatum;

function SchoolYear(year) {
    // School years, like financial years, are denoted by their ending year.
    // For example, the 2010-2011 school year is denoted by 2011.
    // The fix method attempts to convert strings like '2010-11' to school years.
    // Use the commonFormat method to generate the school year as yyyy-yyyy.
    CalendarYear.call(this, year);
    this.value = year;
    this.label = 'school ending year';
    this.fix =
        function() {
            if (this.value.length > 4) {
                var commonExp = /(\d{2,4})\s*(-|\/| )\s*(\d{2,4})/;
                var match = commonExp.exec(this.value);
                if (match) {
                    var year1 = Number(match[1]);
                    var year2 = Number(match[3]);
                    if (year1 < 100) year1 += 2000;
                    if (year2 < 100) year2 += 2000;
                    if (year2 = year1 + 1) {
                        this.value = year2;
                        return 1;
                    }
                }
            }
            if (this.value < 100 && this.validate(this.value + 2000)) { 
                this.value += 2000;
                return 1;
            }
            return this.validate() ? 0 : -1;
        };
    this.commonFormat =
        function () {
            if (this.validate())
                return (this.value - 1) + '-' + this.value;
            else
                return this.value;
        };
}
SchoolYear.prototype = new CalendarYear;

function Grade(grade) {
    // The validate method looks for an integer of one or two digits or one of the strings K, PK, or SP.
    // Missing values are, by default, invalid.
    // The fix method tries uppercasing the value. For valid values, fix will strip a leading zero.
    //   If prefer = 'string' fix will add leading zeros to single-digit grades.
    //   If prefer = 'number' fix will strip leading zeros and convert alphabetic designators to numbers.
    // By default, prefer = 'number'.
    // Use the asNumber method to return a grade number, 0 for K, -1 for PK, and 99 for SP.
    // Use the asString method (not the toString method) to add a leading zero to single-digit grade numbers.
    EdDatum.call(this, grade);
    this.value = String(grade);
    this.prefer = 'number';
    this.label = 'grade';
    this.validExp = /^\d{1,2}$|^K$|^PK$|^SP$/;
    this.validNull = false;
    this.asNumber = 
        function (v) {
            if (v === undefined) v = this.value;
            var intExp = /^\d+$/;
            if (intExp.test(v))
                return Number(v);
            else switch (v.toUpperCase()) {
                case 'K':
                    return 0;
                case 'PK':
                    return 0;
                case 'SP':
                    return 99;
                default:
                    return NaN;
            }
        };
    this.asString =
        function (v) {
            if (v === undefined) v = this.value;
            var intExp = /^\d+$/;
            if (intExp.test(v))
                return v.length == 1 ? '0' + String(v) : String(v);
            else
                return String(v);
        };
    this.fix =
        function() {
            var oldVal = this.value;
            if (this.prefer == 'string') {
                this.value = this.value.toUpperCase();
                this.value = this.asString();
                if (this.validate())
                    return this.value == oldVal ? 0 : 1;
                else
                    this.value = oldVal;
            } else {  // 'number'
                if (this.validate()) {
                    this.value = this.asNumber();
                    return this.value == oldVal ? 0 : 1;
                }
                return -1;  // couldn't fix it
            }
        };
}
Grade.prototype = new EdDatum;

var MCASSubjectName = {
    // MCAS subject names indexed by subject code.
    // Change the values here to control how subject names appear.
    10: 'English',
    20: 'math',
    30: 'science',
    31: 'biology',
    32: 'chemistry',
    33: 'physics',
    34: 'technology/engineering',
};

function MCASSubjectCode(code) {
    // MCAS test subject codes include 10, 20, and 30-33 at present.
    // Missing values are, by default, invalid.
    // The constructor accepts numeric codes.
    // It will attempt to interpret subject names 
    //   and store the corresponding code *at construction time*.
    // Prefer this storage format for an MCAS subject category over storing the subject name directly.
    // Use MCASSubjectName[code] to retrieve subject name strings.
    EdDatum.call(this, code);
    this.prefer = 'number';
    this.label = 'MCAS subject code';
    this.formatter = new google.visualization.NumberFormat( { fractionDigits: 1 } );
    this.validNull = false;
    this.validate =
        function() {
            if (this.code == null && this.validNull) return true;
            switch (this.code) {
                case 10:
                case 20:
                case 30:
                case 31:
                case 32:
                case 33:
                    return true;
                default:
                    return false;
            }
        };
    if (!this.validate()) {
        if (/english|language|ela|reading/i.test(code))
            this.value = 10;
        else if (/math/i.test(code))
            this.value = 20;
        else if (/science/i.test(code))
            this.value = 30;
        else if (/bio/i.test(code))
            this.value = 31;
        else if (/chem/i.test(code))
            this.value = 32;
        else if (/physic/i.test(code))
            this.value = 33;
        else if (/tech/i.test(code))
            this.value = 34;
        // Otherwise keep whatever value was supplied.
    }
}
MCASSubjectCode.prototype = new EdDatum;

function Category(value) {
    // A generic category type.
    // Missing values are valid, by default.
    EdDatum.call(this, value);
    this.label = 'category';
}
Category.prototype = new EdDatum;

function BooleanCategory(value) {
    // A yes/no category, which can be supplied as 1/0, true/false, etc.
    // After running the fix method, the value will be Y, N, or missing, unless the fix fails.
    // To interpret missing values as no's, set the property missingMeansNo to true and run the fix method.
    Category.call(this, value);
    this.label = 'yes/no';
    this.validExp = /^y|^n|^1|^0|^t|^f/i;
    this.missingMeansNo = false;
    this.fix =
        function() {
            var normalExp = /^Y$|^N$/;
            var yesExp = /^y|^1|^t/i;
            var noExp = /^n|^0|^f/i;
            if (normalExp.test(this.value)) return 0;  // no fix needed
            if (this.value == null) {
                if (this.missingMeansNo) {
                    this.value = 'N';
                    return 1;  // fixed it
                }
                if (this.validNull) 
                    return 0;  // okay as missing value
                return -1;  // couldn't fix it
            }
            if (yesExp.test(this.value)) {
                this.value = 'Y';
                return 1;  // fixed it
            }
            if (noExp.test(this.value)) {
                this.value = 'N';
                return 1;  // fixed it
            }
            return -1;  // couldn't fix it
        };
}
BooleanCategory.prototype = new Category;

function Rating(value) {
    // A text rating.
    // Override the order method to assign ordinal numbers to rating values.
    EdDatum.call(this, value);
    this.value = String(value);
    this.prefer = 'string';
    this.label = 'rating';
    this.order = function() {return null};
}
Rating.prototype = new EdDatum;

function Score(value) {
    // A numeric score.
    EdDatum.call(this, value);
    this.value = numberOrMissing(value);
    this.prefer = 'number';
    this.label = 'score';
}
Score.prototype = new EdDatum;

function PointScore(value) {
    // A non-negative, integral score.
    Score.call(this, value);
    this.validRange.min = 0;
    this.validRange.integer = true;
}
PointScore.prototype = new Score;

function DIBELSScore(points) {
    PointScore.call(this, points);
    this.label = 'DIBELS score';
}
DIBELSScore.prototype = new PointScore;

function SRAScore(points) {
    // Fractional scores.
    Score.call(this, points);
    this.label = 'SRA score';
    this.validRange.min = 0;
}
SRAScore.prototype = new Score;

function MCASRawScore(points) {
    // MCAS raw scores for items, tests, aggregate totals.
    PointScore.call(this, points);
    this.label = 'raw score';
}
MCASRawScore.prototype = new PointScore;

function MCASMaximumScore(points) {
    // Maximum points possible.
    MCASRawScore.call(this, points);
    this.label = 'maximum score possible';
}
MCASMaximumScore.prototype = new MCASRawScore;

function MCASAverageScore(points) {
    // Average of raw point scores, may be fractional.
    Score.call(this, points);
    this.label = 'average raw score';
}
MCASAverageScore.prototype = new Score;

function MCASScaledScore(score) {
    // MCAS scaled scores, even values between 200 and 280.
    PointScore.call(this, score);
    this.label = 'scaled score';
    this.validate =
        function() {
            return this.value >= 200 && this.value <= 280 && this.value % 2 == 0;
        };
}
MCASScaledScore.prototype = new PointScore;

function MCASStudentCPI(cpi) {
    // MCAS CPI, comprehensive performance index.
    // This is the individual student value, which can be 0, 25, 50, 75, or 100, or null.
    PointScore.call(this, cpi);
    this.label = 'CPI';
    this.validate =
        function() {
            if (this.value == null) return true;
            switch (this.value) {
                case 0:
                case 25:
                case 50:
                case 75:
                case 100:
                    return true;
                default:
                    return false;
            }
        };
}
MCASStudentCPI.prototype = new PointScore;

var MCASPerformanceLevels = {
    'WF': { long: 'Warning/Failing',   order: 1,   match: /^wf$|^warn\w*.fail/i   },
    'W':  { long: 'Warning',           order: 1.1, match: /^w$|^warn/i            },
    'F':  { long: 'Failing',           order: 0.9, match: /^f$|^fail/i            },
    'NI': { long: 'Needs Improvement', order: 2,   match: /^ni$|^needs/i          },
    'P':  { long: 'Proficient',        order: 3,   match: /^p$|^prof/i            },
    'A':  { long: 'Advanced',          order: 4,   match: /^a$|^adv/i             },
    'P+': { long: 'Beyond Proficient', order: 4.1, match: /^p\+$|^prof\w* (\+|plus)|^beyond prof/i }
};

function MCASPerformanceLevelRecognizer(s) {
    // Attempt to recognize MCAS performance levels.
    // Return the short name if found, otherwise return null.
    if (s == null) return null;
    for (var i in MCASPerformanceLevels)
        if (MCASPerformanceLevels[i].match.test(s))
            return i;
    return null;
}

function MCASPerformanceLevel(value) {
    // MCAS performance levels are Failing, Warning, Needs Improvement, Proficient, Beyond Proficient, and Advanced.
    // Some abbreviations: F, W, WF, NI, P, P+, A.
    // MCAS ALT performance levels include INP, PRG.
    // Other codes slipped into performance levels are ABS, MED, ELL, PAS,...
    Rating.call(this, value);
    this.order = 
        function() {
            var quick = MCASPerformanceLevels[this.value];
            if (quick) return quick.order;
            var shortName = MCASPerformanceLevelRecognizer(this.value);
            if (shortName == null) 
                return 99;
            else
                return MCASPerformanceLevels[shortName].order;
        };
}
MCASPerformanceLevel.prototype = new Rating;

function MCASSGP(value) {
    // Student growth percentiles.
    Score.call(this, value);
    this.label = 'student growth percentile';
    this.validRange.min = 1;
    this.validRange.max = 99;
}
MCASSGP.prototype = new Score;


edd.loaded.eddata = true;
